/**
 * Copyright (C) 2013 Christos Koufogiannakis
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 *     
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.easychart.chart;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;

/**
 * 
 * @author Christos Koufogiannakis
 */
class ChartBaseClass extends View {
	// drawing parameters for this chart
	ChartParameters parameters = new ChartParameters();
	
	// the following paremeters will be calculated based on  the screen (view) size
	protected float plotTitleTextSize;
	private float 	xTitleTextSize;
	private float 	yTitleTextSize;
	private float 	xLabelTextSize;
	private float 	yLabelTextSize;
	private float 	legendTextSize;
	protected float vertexLabelTextSize;
	private String 	longestYLabel;
	private int 	numberOfYLabels;
	private double	yLabelInterval;
	protected float maxPointHeight;
	protected int 	viewHeight;		
	protected int 	viewWidth;			                                                                           
	protected float plotLeftX = 0;
	protected float plotRightX;
	protected float plotTopY;
	protected float plotBottomY;
	protected float plotBottomYInside;
	private float 	plotHeight;
	protected float plotWidth;
	protected float legendTopY;	
	
	protected Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
	protected Rect bounds = new Rect();

	// colors used 
	ArrayList<Integer> colorsArray = new ArrayList<Integer>(
			Arrays.asList(Color.GREEN, Color.BLUE, Color.RED, Color.MAGENTA,
					Color.YELLOW, Color.CYAN, Color.GRAY, Color.DKGRAY));	
	
	// calculated from input Y values
	protected double minY = Double.MAX_VALUE;
	protected double maxY = Double.MIN_VALUE;
	protected double yLabelRange;
	protected double minYLabel;	
	protected double maxYLabel;
	private int yLabelZeroRemovalCounter=0;
	private boolean removeYLabelTrailingZeros = true;
	
	// user input
	protected List<String> xLabels = new ArrayList<String>();
	private String plotTitle = "";
	private String xTitle = "";
	private String yTitle = "";
	protected String [] legendLabels = null;
	
	/**
	 * Constructor for the base chart class.
	 * 
	 * @param context
	 * @param attrs
	 */
	public ChartBaseClass(Context context, AttributeSet attrs) {
		super(context, attrs);
		initializeBase();
	}
	
	/**
	 * Constructor for the base chart class
	 * 
	 * @param context
	 */
	public ChartBaseClass(Context context) {
		super(context);
		initializeBase();
	}
	
	/**
	 * Initialize the base chart class
	 */
	protected void initializeBase() {
		mPaint.setStyle(Style.FILL); 
		mPaint.setFakeBoldText(true);
		
		// auto calculate the text size to be use in different places
		float textSize = mPaint.getTextSize() *	getResources().getDisplayMetrics().density;
		plotTitleTextSize = 1.5f * textSize;
		xTitleTextSize = 1.2f * textSize;
		yTitleTextSize =  1.2f * textSize;
		xLabelTextSize = textSize;
		yLabelTextSize = textSize;
		vertexLabelTextSize = textSize;	
		legendTextSize = textSize;	
	}
	
	/**
	 * Set plot title
	 * 
	 * @param plotTitle
	 */
	public void setPlotTitle(String plotTitle) {
		if (plotTitle != null) {
			this.plotTitle = plotTitle;		
		}
	}
	
	/**
	 * Set x-axis title
	 * 
	 * @param xTitle
	 */
	public void setXTile(String xTitle) {
		if (xTitle != null) {
			this.xTitle = xTitle;		
		}
	}
	
	/**
	 * Set y-axis title
	 * 
	 * @param yTitle
	 */
	public void setYTile(String yTitle) {
		if (yTitle != null) {
			this.yTitle = yTitle;		
		}
	}
	
	/**
	 * Set x-axis labels
	 * 
	 * @param xLabels
	 */
	public void setXLabels(List<String> xLabels) {
		if (xLabels != null) {
			this.xLabels = xLabels;		
		}
	}

	/**
	 * Method that creates scale for y-axis labels. Before calling this 
	 * method make sure that minY and maxY are already set. 
	 * 
	 * <p>The method used for auto-scaling the axis is described by Don Li in his paper 
	 *  <a href="http://www.lexjansen.com/pharmasug/2003/coderscorner/cc024.pdf">
	 *  Tired of Defining Axis Scale for SAS Graphs</a></p>
	 *  
	 */
	protected void createYLabels() {
		double roundedMin = Math.floor(minY > 0 ? 0:minY);
		double roundedMax = Math.ceil(maxY < 0 ? 0:maxY);
		double roundedRange = roundedMax - roundedMin;

		// unit is the number of 10s that we can fit in the range
		double unit = roundedRange/10; 		
		
		// grade is the highest power of ten that is less than the unit
		// f.e if unit is 300, then grade = 2
		double grade = Math.floor(Math.log10(unit));
		
		// 10 to the grade
		double tenToGrade = Math.pow(10,grade);
		
		// normalize unit
		double normalizedUnit= unit/tenToGrade;						
		
		// test sqrt(2), sqrt(10) and sqrt(50) as cut-off points 
		if (normalizedUnit < Math.sqrt(2)) {
			// use 1-based intervals
			yLabelInterval = tenToGrade * 1;
		}
		else{
			if (normalizedUnit < Math.sqrt(10)) {
				// use 2-based intervals
				yLabelInterval = tenToGrade * 2;
			}
			else{
				if (normalizedUnit < Math.sqrt(50)) {
					// use 5-based intervals
					yLabelInterval = tenToGrade * 5;
				}
				else {
					// use 10-based intervals
					yLabelInterval = tenToGrade * 10;
				}
			}
		}

		// calculate the max and min Y-axis labels
		maxYLabel = Math.ceil(roundedMax/yLabelInterval) * yLabelInterval;
		minYLabel = Math.floor(roundedMin/yLabelInterval) * yLabelInterval;
		
		// calculated the Y-axis label range
		yLabelRange = maxYLabel - minYLabel;
		
		// calculate the number of labels (ticks) in the Y-axis
		numberOfYLabels = (int) (1 + yLabelRange/yLabelInterval);
		
		// find the longest Y-axis label. Also see if the we can remove
		// the trailing zeros from all the labels. For example if all labels
		// are like x.0, then the .0 part can be removed from all of them
		longestYLabel = "";
		double curr_label = minYLabel;
		double longest_label = minYLabel;
		yLabelZeroRemovalCounter = 0;
		for (int i = 0; i < numberOfYLabels; i++){
			String label = labelValueToString(curr_label, true);
			if (longestYLabel.length() < label.length()){
				longest_label = curr_label;
				longestYLabel = label;
			}
			curr_label += yLabelInterval;
		}
		
		if (yLabelZeroRemovalCounter == numberOfYLabels){
			// all Y-axis labels have .0 so all these zeros can be removed
			removeYLabelTrailingZeros = true;
		}
		else{
			// not all Y-axis labels end with .0, so we cannot remove it from any of them
			removeYLabelTrailingZeros = false;
			longestYLabel = labelValueToString(longest_label, false);
		}
	}
		
	/**
	 * This method needs to be called to initialize the background.
	 * 
	 * @param canvas
	 */
	protected void initBackground(Canvas canvas){
		// get view height in pixels
		viewHeight = getHeight();	
		// get view width in pixels
		viewWidth = getWidth();		
		
		//set background color
		canvas.drawColor(parameters.BACKGROUND_COLOR.getValue());		
	}
	
	@Override
	protected void onDraw(Canvas canvas) {
		createYLabels();
		initBackground(canvas);
		
		// get max y axis label width
		mPaint.setTextSize(yLabelTextSize);
		mPaint.getTextBounds(longestYLabel, 0, longestYLabel.length(), bounds);
		
		plotLeftX = parameters.YTITLE_LEFT_TO_BORDER.getValue() + yTitleTextSize
				+ parameters.YTITLE_RIGHT_TO_YLABEL.getValue() + bounds.width() +
				parameters.YLABEL_TO_PLOT.getValue();	
		drawLegend(canvas);
		
		plotRightX = viewWidth - parameters.PLOT_RIGHT_TO_BORDER.getValue();
		
		drawPlotTitle(canvas);
		plotTopY = parameters.PLOT_TITLE_TOP_TO_BORDER.getValue() +
				plotTitleTextSize + parameters.PLOT_TITLE_BOTTOM_TO_PLOT.getValue();
		
		//mLegendTopY will be equal to mViewHeight when legend is off
		plotBottomY = legendTopY - (parameters.XTITLE_BOTTOM_TO_BORDER.getValue() 
				+ xTitleTextSize + parameters.XTITLE_TOP_TO_XLABEL.getValue()
				+ xLabelTextSize + parameters.XLABEL_TO_PLOT.getValue());
		plotBottomYInside =  plotBottomY - parameters.PLOT_BORDER_STROKE.getValue()/2;
		plotHeight = plotBottomY - plotTopY - parameters.PLOT_BORDER_STROKE.getValue();
		plotWidth = plotRightX - plotLeftX - parameters.PLOT_BORDER_STROKE.getValue();
		
//		if (SHOW_VERTEX_LABELS == 1)
//			mMaxPointHeight = mPlotHeight - VERTEX_TO_VERTEX_LABEL - mVertexLabelTextSize - MIN_VERTEX_LABEL_TO_PLOT;
//		else
			maxPointHeight = plotHeight;
			
		drawPlotBorders(canvas);
		drawXTitle(canvas);
		drawYTitle(canvas);	
		drawYLabels(canvas);				
	}
	
	/**
	 * This method draws the y-axis labels
	 * 
	 * @param canvas
	 */
	private void drawYLabels(Canvas canvas){		
		mPaint.setTextSize(yLabelTextSize);
		mPaint.setTextAlign(Align.RIGHT);
		
		int totalYLabelHeight = 0;
		for (int i = 0; i < numberOfYLabels; i++){		
			String labelStr =  labelValueToString((double) (minYLabel + i*yLabelInterval), removeYLabelTrailingZeros);	
			mPaint.getTextBounds(labelStr, 0, labelStr.length(), bounds);
			totalYLabelHeight += bounds.height()+1; // +1 for an empty pixel between two labels
		}	
			
		int divideFactor = (int) Math.ceil(totalYLabelHeight/maxPointHeight);
		if (divideFactor == 0) divideFactor = 1;
		
		int step = 0;
		float x = plotLeftX - parameters.YLABEL_TO_PLOT.getValue();
		float yStep = maxPointHeight/ (numberOfYLabels-1);
		
		for (int i = 0; i < numberOfYLabels; i++){		
			String labelStr =  labelValueToString((double) (minYLabel + i*yLabelInterval), removeYLabelTrailingZeros);	
			
			float y = plotBottomYInside - i * yStep;
			
			if (step == 0){
				mPaint.setColor(parameters.LABEL_TEXT_COLOR.getValue());	
				canvas.drawText(labelStr, x , y + yLabelTextSize/3, mPaint);
				
				mPaint.setColor(parameters.PLOT_BORDER_COLOR.getValue());
				mPaint.setStrokeWidth(parameters.LABEL_LINE_STROKE.getValue());
				//small horizontal line between ylabel and vertical plot border
				canvas.drawLine(plotLeftX, y, plotLeftX - parameters.LABEL_LINE_LENGTH.getValue(), y, mPaint); 
			}
			
			step = (step+1) % divideFactor;
			
			if (parameters.SHOW_ALL_LABEL_LINES.getValue()){
				mPaint.setColor(parameters.PLOT_BORDER_COLOR.getValue());
				mPaint.setStrokeWidth(parameters.LABEL_LINE_STROKE.getValue());
				//small horizontal line between ylabel and vertical plot border
				canvas.drawLine(plotLeftX, y, plotLeftX - parameters.LABEL_LINE_LENGTH.getValue(), y, mPaint);
			}
			
			if (parameters.SHOW_HORIZONTAL_GRID.getValue() && i!=0 && i != numberOfYLabels-1){
				for (float x_grid = plotLeftX; x_grid < plotRightX - parameters.GRID_LINE_LENGTH.getValue();
						x_grid += 2 * parameters.GRID_LINE_LENGTH.getValue())
					canvas.drawLine(x_grid, y, x_grid + parameters.GRID_LINE_LENGTH.getValue(), y, mPaint);
			}
		}		
	}
	
	/**
	 * Draws the title of the graph
	 * 
	 * @param canvas
	 */
	protected void drawPlotTitle(Canvas canvas){	
		mPaint.setColor(parameters.TITLE_TEXT_COLOR.getValue());	
		mPaint.setTextAlign(Align.CENTER);
		
		// return reduced text size if necessary and sets bounds
		plotTitleTextSize = adjustTextSize(plotTitle, plotTitleTextSize, viewWidth);
				
		if (bounds.width() < plotRightX - plotLeftX )
			canvas.drawText(plotTitle, (plotLeftX + plotRightX )/ 2 ,
					(parameters.PLOT_TITLE_TOP_TO_BORDER.getValue() + plotTitleTextSize), mPaint);  
		else
			canvas.drawText(plotTitle, viewWidth/ 2 , 
					(parameters.PLOT_TITLE_TOP_TO_BORDER.getValue() + plotTitleTextSize), mPaint);  
	}
	
	/**
	 * Draws the y-axis title
	 * 
	 * @param canvas
	 */
	private void drawYTitle(Canvas canvas){
		mPaint.setTextAlign(Align.CENTER);
		mPaint.setColor(parameters.TITLE_TEXT_COLOR.getValue());
	
		// return and sets reduced text size if necessary and sets bounds
		yTitleTextSize = adjustTextSize(yTitle, yTitleTextSize, plotBottomY - plotTopY);
		
		float x = parameters.YTITLE_LEFT_TO_BORDER.getValue() + yTitleTextSize;
		float y = (plotTopY + plotBottomY)/2;
		canvas.rotate(-90, x, y);	
		// drawText (String text, float x, float y, mPaint mPaint)
		canvas.drawText(yTitle, x, y, mPaint); 
		canvas.rotate(90, x, y);
	}
	
	/**
	 * Draw x-axis title
	 * 
	 * @param canvas
	 */
	private void drawXTitle(Canvas canvas){
		mPaint.setColor(parameters.TITLE_TEXT_COLOR.getValue());	
		mPaint.setTextAlign(Align.CENTER);	
		
		// return and sets reduced text size if necessary and sets bounds
		xTitleTextSize = adjustTextSize(xTitle, xTitleTextSize, plotRightX - plotLeftX);
			
		//mLegendTopY will be equal to mViewHeight when legend is off
		canvas.drawText(xTitle, (plotLeftX + plotRightX)/ 2,
				(legendTopY - parameters.XTITLE_BOTTOM_TO_BORDER.getValue())
				, mPaint);
	}

	/**
	 * Draw the plot border lines
	 * 
	 * @param canvas
	 */
	private void drawPlotBorders(Canvas canvas){
		//drawRect (float left, float top, float right, float bottom, mPaint mPaint) 
		mPaint.setColor(parameters.PLOT_BACKGROUND_COLOR.getValue());
		mPaint.setStyle(Style.FILL);
		canvas.drawRect(plotLeftX, plotTopY, plotRightX, plotBottomY, mPaint);	
		
		// line thickness
		mPaint.setStrokeWidth(parameters.PLOT_BORDER_STROKE.getValue());				
		mPaint.setColor(parameters.PLOT_BORDER_COLOR.getValue());			
		
		if (parameters.SHOW_BOTTOM_PLOT_BORDER.getValue()) {
			// top horizontal plot border line
			canvas.drawLine(plotLeftX, plotTopY, plotRightX, plotTopY, mPaint);	 
		}
		if (parameters.SHOW_LEFT_PLOT_BORDER.getValue()) {
			// left vertical plot border line 
			canvas.drawLine(plotLeftX, plotTopY, plotLeftX, plotBottomY, mPaint);
		}
		if (parameters.SHOW_RIGHT_PLOT_BORDER.getValue()){
			// right vertical plot border line
			canvas.drawLine(plotRightX, plotTopY, plotRightX, plotBottomY, mPaint);
		}
		
		drawsXAxis(canvas);
	}
	
	/**
	 * Draw x-axis labels
	 * 
	 * @param canvas
	 * @param spaceBetweenLabels
	 * @param offset
	 */
	protected void drawXLabels(Canvas canvas, float spaceBetweenLabels, int offset){
		mPaint.setStyle(Style.FILL);
		mPaint.setTextSize(xLabelTextSize);
	
		int totalXLabelLength = 0;
		for  (int i=0; i < xLabels.size(); i++){
			mPaint.getTextBounds(xLabels.get(i), 0, xLabels.get(i).length(), bounds);
			totalXLabelLength += bounds.width()+2; // +2 for an empty char between two labels
		}
		
		int divideFactor = (int) Math.ceil(totalXLabelLength/plotWidth);
		if (divideFactor == 0) divideFactor = 1;
		
		int step = 0;
		float x = plotLeftX + offset;
		float y = plotBottomY + parameters.XLABEL_TO_PLOT.getValue() + xLabelTextSize;
		
		mPaint.setTextAlign(Align.CENTER);
		for (int i=0; i < xLabels.size(); i++){
			String label = xLabels.get(i);	
					
			if (step == 0){
				mPaint.setColor(parameters.LABEL_TEXT_COLOR.getValue());	
				canvas.drawText(label, x , y, mPaint);
				
				mPaint.setColor(parameters.PLOT_BORDER_COLOR.getValue());
				mPaint.setStrokeWidth(parameters.LABEL_LINE_STROKE.getValue());
				// small vertical between xlabel and horizontal plot border
				canvas.drawLine(x, plotBottomY, x, plotBottomY + parameters.LABEL_LINE_LENGTH.getValue(), mPaint);	
			}
			
			step = (step+1) % divideFactor;
					
			if (parameters.SHOW_ALL_LABEL_LINES.getValue()){
				mPaint.setColor(parameters.PLOT_BORDER_COLOR.getValue());
				mPaint.setStrokeWidth(parameters.LABEL_LINE_STROKE.getValue());
				// small vertical between xlabel and horizontal plot border
				canvas.drawLine(x, plotBottomY, x, plotBottomY + parameters.LABEL_LINE_LENGTH.getValue(), mPaint);
			}
			
			if (parameters.SHOW_VERTICAL_GRID.getValue()){
				for (float y_grid = plotTopY; 
						y_grid < plotBottomY - parameters.GRID_LINE_LENGTH.getValue();
						y_grid += 2 * parameters.GRID_LINE_LENGTH.getValue())
					canvas.drawLine(x, y_grid, x, y_grid + parameters.GRID_LINE_LENGTH.getValue(), mPaint);
			}
			
			x += spaceBetweenLabels;
		}
	}	
	
	// this function sets mLegendTopY which is need for the rest of the functions
	protected void drawLegend(Canvas canvas){
		if (parameters.SHOW_LEGEND.getValue() && legendLabels != null){
			mPaint.setTextAlign(Align.LEFT);
			mPaint.setTextSize(legendTextSize);
			mPaint.setStyle(Style.FILL);
			
			float xOffset = parameters.LEGEND_LEFT_TO_BORDER.getValue();  
			// float xOffset = (float) max(LEGEND_LEFT_TO_BORDER, mPlotLeftX);
			
			float x = xOffset;
			float y = viewHeight - parameters.LEGEND_BOTTOM_TO_BORDER.getValue();
						
			for (int i=0; i < legendLabels.length; i++){	
				mPaint.setColor(colorsArray.get(i));					
				canvas.drawCircle(x, y - parameters.LEGEND_SYMBOL_WIDTH.getValue() - 1 ,
						parameters.LEGEND_SYMBOL_WIDTH.getValue(), mPaint);
				
				x += parameters.LEGEND_SYMBOL_WIDTH.getValue() 
						+ parameters.LEGEND_SYMBOL_TO_TEXT.getValue();
				mPaint.setColor(parameters.LEGEND_TEXT_COLOR.getValue());
				canvas.drawText(legendLabels[i], x , y, mPaint);
								
				mPaint.getTextBounds(legendLabels[i], 0, legendLabels[i].length(), bounds);		
				x += bounds.width() + parameters.LEGEND_SPACE_TO_RIGHT_LABEL.getValue() 
						+ parameters.LEGEND_SYMBOL_WIDTH.getValue()/2;
							
				// if the next legend does not fit in this line, move to one line up
				if (i+1 <legendLabels.length){
					mPaint.getTextBounds(legendLabels[i+1], 0, legendLabels[i+1].length(), bounds);		

					if (x  + parameters.LEGEND_SYMBOL_WIDTH.getValue() 
							+ parameters.LEGEND_SYMBOL_TO_TEXT.getValue() + bounds.width()  > viewWidth){
						x = xOffset;
						y = y - (parameters.LEGEND_SYMBOL_WIDTH.getValue() > legendTextSize ?
								parameters.LEGEND_SYMBOL_WIDTH.getValue() :
									legendTextSize) - parameters.LEGEND_SYMBOL_TO_TEXT.getValue();
					}
				}
			}
			
			legendTopY = y - (parameters.LEGEND_SYMBOL_WIDTH.getValue() > legendTextSize ?
					parameters.LEGEND_SYMBOL_WIDTH.getValue() :
						legendTextSize) - parameters.LEGEND_SPACE_TO_TOP_LABEL.getValue();
		}
		else{
			legendTopY = viewHeight;
		}
		
		legendLabels = null;
	};
	
	/**
	 * Redraws chart
	 */
	public void refresh(){
		invalidate();
	}
	
	/**
	 * Draws x-axis line
	 * 
	 * @param canvas
	 */
	protected void drawsXAxis(Canvas canvas){
		if (parameters.SHOW_HORIZONTAL_AXIS_AT_ZERO.getValue()){
			float y = (float) (plotBottomYInside - maxPointHeight * Math.abs(minYLabel)/(yLabelRange));
			
			mPaint.setColor(parameters.PLOT_BORDER_COLOR.getValue());
			mPaint.setStrokeWidth(0);
			
			canvas.drawLine(plotLeftX, y, plotRightX, y, mPaint);
		}
	}
	
	/**
	 * Adjust text size to fit the given specs
	 * 
	 * @param text
	 * @param textSize
	 * @param limit
	 * @return
	 */
	protected float adjustTextSize(String text, float textSize, float limit){
		mPaint.setTextSize(textSize);
		mPaint.getTextBounds(text, 0, text.length(), bounds);
		
		// reduce the textSize until it fits
		while (bounds.width()+2 > limit && textSize > 0){
			textSize -= 1;	
			mPaint.setTextSize(textSize);
			mPaint.getTextBounds(text, 0, text.length(), bounds);
		}
		
		return textSize;
	}
				
	/**
	 * Converts a double value to a String with up to two fractional digits
	 * 
	 * @param value the double to be converted
	 * @param removeTrailingZeros if true, then it will remove the fractional
	 * part from the string representation if it is like x.0
	 * @return a String representation of the double with up to two fractional digits
	 */
	protected String labelValueToString(double value, boolean removeTrailingZeros){
		// round to 2 decimal places, i.e. #.##
		double label_number = Math.round(value*100)/100.0d; 
		String str = Double.toString(label_number);
		
		String tail = str.substring(str.length()-2, str.length());
		if (tail.compareTo(".0") == 0 && removeTrailingZeros){
			// number of times that rounding has been done
			yLabelZeroRemovalCounter++;
			return str.replace(".0", "");
		}
		
		return str;	
	}
	
	/**
	 * Get the parameters of this chart
	 * 
	 * @return
	 */
	public ChartParameters getParameters(){
		return this.parameters;
	}
		
	/**
	 * Checks if a list is null
	 * 
	 * @throws IllegalArgumentException if list is null
	 */
	static void checkNullity(List<?> values) throws IllegalArgumentException {
		if (values == null) {
			throw new IllegalArgumentException("values must not be null or empty");
		}
	}
}
